"""应用主窗口。"""

from __future__ import annotations

import json
import logging
import uuid

from typing import Literal

from PySide6.QtCore import QObject, Qt, QThread, QTimer, Signal, Slot
from PySide6.QtWidgets import (
    QDialog,
    QFormLayout,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QScrollArea,
    QSizePolicy,
    QSplitter,
    QStatusBar,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

from src.config import (
    PUBLISH_DEFAULTS,
    has_mqtt_user_config,
    load_mqtt_user_config,
    save_mqtt_user_config,
    has_db_user_config,
    load_db_user_config,
    save_db_user_config,
    load_topic_template_config,
    load_message_template_config,
    save_topic_template_config,
    save_message_template_config,
)
from src.core.audit import audit_logger
from src.core.db_connector import MySQLConnector, DeviceDataManager
from src.core.models import Device
from src.core.repository import DeviceRepository
from src.services.bool_policy import coerce_bool_value, detect_bool_scheme, format_bool_for_display
from src.services.data_generator import AttributeValueGenerator
from src.services.mqtt_client import MQTTConfig, MQTTService
from src.services.template_validator import MessageTemplateValidator
from src.ui.device_dialog import DeviceDialog
from src.ui.device_table import DeviceTable
from src.ui.connection_sidebar import ConnectionSidebar
from src.ui.category_column import CategoryColumn
from src.ui.template_dialog import TemplateEditDialog


logger = logging.getLogger(__name__)


class RepositoryLoader(QObject):
    """在后台线程中从数据库加载设备数据仓库。"""

    finished = Signal(object)
    failed = Signal(str)

    def __init__(self, db_config: dict) -> None:
        """
        初始化数据加载器。
        
        Args:
            db_config: 数据库配置字典
        """
        super().__init__()
        self._db_config = db_config

    @Slot()
    def load(self) -> None:
        """从数据库加载设备数据。"""
        import logging
        logger = logging.getLogger(__name__)
        
        try:
            logger.info("开始连接数据库...")
            db_connector = MySQLConnector(
                host=self._db_config["host"],
                port=self._db_config["port"],
                database=self._db_config["database"],
                user=self._db_config["user"],
                password=self._db_config["password"],
            )
            
            if not db_connector.is_connected():
                raise RuntimeError("数据库连接失败")
            
            logger.info("数据库连接成功，开始加载数据...")
            data_manager = DeviceDataManager(db_connector)
            raw_data_count = len(data_manager.get_all_device_info())
            logger.info(f"从数据库获取到 {raw_data_count} 条设备记录")
            
            repository = DeviceRepository.from_database(data_manager)
            
            # 验证数据
            root_nodes = repository.get_root_nodes()
            all_nodes = repository.get_all_type_nodes()
            device_count = len(repository._devices)
            
            logger.info(f"数据仓库创建完成：根节点={len(root_nodes)}, 总节点={len(all_nodes)}, 设备={device_count}")
            
            if not root_nodes and device_count > 0:
                logger.warning("有设备数据但无根节点，可能是数据关联问题")
            
            # 关闭数据库连接
            db_connector.close()
            logger.info("数据库连接已关闭")
            
        except Exception as exc:  # noqa: BLE001
            logger.exception("加载数据时发生错误：%s", exc)
            self.failed.emit(str(exc))
            return
        
        self.finished.emit(repository)


class MainWindow(QMainWindow):
    """PySide6 主界面，统筹分类导航与设备操作。"""

    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("IoT 设备属性测试客户端")
        self.resize(1400, 700)  # 增加窗口宽度以适应三列布局

        self._repository: DeviceRepository | None = None
        self._repository_thread: QThread | None = None
        self._repository_loader: RepositoryLoader | None = None
        self._db_test_thread: QThread | None = None  # 数据库连接测试线程
        self._generator = AttributeValueGenerator()
        self._mqtt_service = MQTTService()
        # 使用 QueuedConnection 确保信号在主线程中处理，避免UI更新问题
        self._mqtt_service.connecting.connect(self._on_mqtt_connecting, Qt.ConnectionType.QueuedConnection)
        self._mqtt_service.connected.connect(self._on_mqtt_connected, Qt.ConnectionType.QueuedConnection)
        self._mqtt_service.disconnected.connect(self._on_mqtt_disconnected, Qt.ConnectionType.QueuedConnection)
        self._mqtt_service.connection_failed.connect(self._on_mqtt_connection_failed, Qt.ConnectionType.QueuedConnection)
        # 绑定MQTT发布成功信号，保存发布历史
        self._mqtt_service.publish_succeeded.connect(self._on_mqtt_publish_succeeded, Qt.ConnectionType.QueuedConnection)

        self._connection_sidebar = ConnectionSidebar()
        self._category_column = CategoryColumn()
        self._device_table = DeviceTable()
        self._current_devices: list[Device] = []  # 保存当前分类的所有设备，用于搜索过滤
        self._current_category_id: int | None = None  # 记录当前选中的物模型类别ID
        self._current_category_attributes = []  # 当前分类的属性列表
        
        # 存储每个设备的发布历史（device_id -> 最后一次发布的payload）
        self._device_publish_history: dict[int, dict] = {}
        
        # 随机发布相关
        self._random_publish_timer = QTimer(self)
        self._random_publish_timer.timeout.connect(self._on_random_publish_timer)
        self._is_random_publishing = False
        self._random_publish_interval = PUBLISH_DEFAULTS.interval_seconds  # 保存当前间隔值

        initial_mqtt = load_mqtt_user_config()
        initial_db = load_db_user_config()
        
        # 加载Topic和消息模板配置
        self._topic_template = load_topic_template_config().template
        self._message_template = load_message_template_config().template

        # MQTT配置输入框
        self._host_input = QLineEdit(initial_mqtt.host)
        self._port_input = QLineEdit(str(initial_mqtt.port))
        self._username_input = QLineEdit(initial_mqtt.username or "")
        self._password_input = QLineEdit(initial_mqtt.password or "")
        self._password_input.setEchoMode(QLineEdit.EchoMode.Password)
        
        # 创建显示/隐藏密码的按钮
        self._password_visibility_btn = QPushButton("👁")
        self._password_visibility_btn.setFixedSize(28, 28)
        self._password_visibility_btn.setToolTip("显示/隐藏密码")
        self._password_visibility_btn.clicked.connect(self._toggle_password_visibility)
        
        # 创建显示/隐藏账号的按钮（账号默认显示，所以初始图标为👁‍🗨）
        self._username_visibility_btn = QPushButton("👁‍🗨")
        self._username_visibility_btn.setFixedSize(28, 28)
        self._username_visibility_btn.setToolTip("显示/隐藏账号")
        self._username_visibility_btn.clicked.connect(self._toggle_username_visibility)
        
        # 记录密码和账号的显示状态
        # 密码默认隐藏，账号默认显示
        self._password_visible = False
        self._username_visible = True
        
        self._status_label = QLabel("未连接")
        self._status_indicator = QLabel()
        self._status_indicator.setFixedSize(12, 12)

        self._set_status("未连接", "disconnected")

        # 数据库配置输入框
        self._db_host_input = QLineEdit(initial_db.host)
        self._db_port_input = QLineEdit(str(initial_db.port))
        self._db_database_input = QLineEdit(initial_db.database)
        self._db_user_input = QLineEdit(initial_db.user)
        self._db_password_input = QLineEdit(initial_db.password)
        self._db_password_input.setEchoMode(QLineEdit.EchoMode.Password)
        
        # 数据库密码显示/隐藏按钮
        self._db_password_visibility_btn = QPushButton("👁")
        self._db_password_visibility_btn.setFixedSize(28, 28)
        self._db_password_visibility_btn.setToolTip("显示/隐藏密码")
        self._db_password_visibility_btn.clicked.connect(self._toggle_db_password_visibility)
        self._db_password_visible = False
        
        # 数据库连接状态
        self._db_status_label = QLabel("未连接")
        self._db_status_indicator = QLabel()
        self._db_status_indicator.setFixedSize(12, 12)
        self._set_db_status("未连接", "disconnected")
        
        # 数据库连接器（用于测试连接）
        self._db_connector: MySQLConnector | None = None

        # 用于控制连接失败弹窗只显示最后一次
        self._connection_failed_timer = QTimer(self)
        self._connection_failed_timer.setSingleShot(True)
        self._connection_failed_timer.timeout.connect(self._show_connection_failed_dialog)
        self._pending_failure_info: tuple[int, str] | None = None  # (rc, message)
        self._is_showing_failure_dialog = False  # 标记是否正在显示弹窗

        self._build_ui()
        self._bind_events()
        self._configure_service_with_current_inputs()
        # 窗口居中显示（适配1080p屏幕）
        self._center_window()
        # 使用单次定时器，在界面显示后异步尝试连接数据库和MQTT
        QTimer.singleShot(100, self._try_auto_connect_all)

    # ---------------- UI 构建 ---------------- #

    def _build_ui(self) -> None:
        central = QWidget()
        central_layout = QHBoxLayout(central)
        central_layout.setContentsMargins(8, 8, 8, 8)
        central_layout.setSpacing(8)

        # 设置MQTT配置到连接侧边栏
        connect_btn = QPushButton("连接/更新")
        connect_btn.clicked.connect(self._on_connect_clicked)
        self._connection_sidebar.setup_mqtt_config(
            host_input=self._host_input,
            port_input=self._port_input,
            username_input=self._username_input,
            password_input=self._password_input,
            username_visibility_btn=self._username_visibility_btn,
            password_visibility_btn=self._password_visibility_btn,
            connect_btn=connect_btn,
            status_indicator=self._status_indicator,
            status_label=self._status_label,
        )
        
        # 设置数据库配置到连接侧边栏
        db_connect_btn = QPushButton("连接/加载数据")
        db_connect_btn.clicked.connect(self._on_db_connect_clicked)
        self._connection_sidebar.setup_db_config(
            host_input=self._db_host_input,
            port_input=self._db_port_input,
            database_input=self._db_database_input,
            user_input=self._db_user_input,
            password_input=self._db_password_input,
            password_visibility_btn=self._db_password_visibility_btn,
            connect_btn=db_connect_btn,
            status_indicator=self._db_status_indicator,
            status_label=self._db_status_label,
        )
        
        # 设置模板编辑按钮到连接侧边栏
        edit_topic_template_btn = QPushButton("编辑 Topic 模板")
        edit_topic_template_btn.clicked.connect(self._on_edit_topic_template_clicked)
        edit_message_template_btn = QPushButton("编辑消息模板")
        edit_message_template_btn.clicked.connect(self._on_edit_message_template_clicked)
        self._connection_sidebar.setup_template_buttons(
            edit_topic_template_btn=edit_topic_template_btn,
            edit_message_template_btn=edit_message_template_btn,
        )

        # 创建三列布局的分割器
        splitter = QSplitter(Qt.Orientation.Horizontal)
        # 确保所有列的高度策略一致，使它们高度对齐
        self._connection_sidebar.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
        self._category_column.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
        splitter.addWidget(self._connection_sidebar)  # 第一列：连接侧边栏
        splitter.addWidget(self._category_column)  # 第二列：物模型分类列
        splitter.addWidget(self._build_device_panel())  # 第三列：设备面板
        splitter.setStretchFactor(0, 0)  # 连接侧边栏不拉伸
        splitter.setStretchFactor(1, 0)  # 分类列不拉伸
        splitter.setStretchFactor(2, 1)  # 设备面板拉伸
        # 设置各列的初始宽度（适配1080p屏幕）
        splitter.setSizes([280, 200, 720])  # 连接侧边栏280px，分类列200px，设备面板720px

        central_layout.addWidget(splitter)

        self.setCentralWidget(central)
        self.setStatusBar(QStatusBar())


    def _build_device_panel(self) -> QWidget:
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(8)

        title = QLabel("设备列表")
        title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        title.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

        # 添加搜索框
        search_box = QWidget()
        search_layout = QHBoxLayout(search_box)
        search_layout.setContentsMargins(0, 0, 0, 0)
        search_layout.setSpacing(6)

        search_label = QLabel("搜索：")
        search_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)

        self._search_input = QLineEdit()
        self._search_input.setPlaceholderText("输入关键词搜索设备（支持设备名称、物模型名称、Product Key、设备SN）")
        self._search_input.setMinimumHeight(28)
        self._search_input.textChanged.connect(self._on_search_text_changed)

        search_layout.addWidget(search_label)
        search_layout.addWidget(self._search_input)

        layout.addWidget(title)
        layout.addWidget(search_box)
        
        # 设备列表区域（高度自适应，随窗口大小缩放）
        # 将设备表格放入滚动区域，既能自适应又能处理大量设备
        device_scroll_area = QScrollArea()
        device_scroll_area.setWidget(self._device_table)
        device_scroll_area.setWidgetResizable(True)
        device_scroll_area.setMinimumHeight(120)  # 设置最小高度，确保窗口很小时也能正常显示
        # 移除最大高度限制，让区域随窗口大小自适应缩放
        device_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        device_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        device_scroll_area.setFrameShape(QScrollArea.Shape.NoFrame)  # 去掉边框，保持简洁
        # 设置设备表格的高度策略，让它根据内容自适应
        self._device_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        layout.addWidget(device_scroll_area, 1)  # 设置拉伸因子为1，占据剩余空间，随窗口大小自适应
        
        # 添加属性测试字段（显示当前分类的属性），固定在底部，不随滚动
        self._attribute_form_widget = self._build_attribute_form()
        layout.addWidget(self._attribute_form_widget, 1)  # 设置拉伸因子为1，占据剩余空间，随窗口大小自适应
        
        # 添加随机发布和一键发布按钮，固定在底部
        button_box = self._build_publish_buttons()
        layout.addWidget(button_box, 0)  # 不拉伸，固定在底部
        
        return panel
    
    def _build_attribute_form(self) -> QWidget:
        """构建属性测试表单，显示当前分类的属性，固定在底部，不随滚动条消失。"""
        # 创建容器widget，包含标题和表单内容
        container = QWidget()
        container_layout = QVBoxLayout(container)
        container_layout.setContentsMargins(0, 0, 0, 0)
        container_layout.setSpacing(8)
        
        # 添加"属性测试"标题，与"设备列表"标题样式一致
        attribute_title = QLabel("属性测试")
        attribute_title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        attribute_title.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        container_layout.addWidget(attribute_title)
        
        # 创建滚动区域（用于属性表单内部滚动）
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setMinimumHeight(100)  # 设置最小高度，确保窗口很小时也能正常显示
        # 移除最大高度限制，让区域随窗口大小自适应缩放
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: 1px solid #e0e0e0;
                border-radius: 4px;
                background-color: #fafafa;
            }
        """)
        
        # 创建组框（不再使用标题）
        box = QGroupBox()
        box.setStyleSheet("""
            QGroupBox {
                font-weight: bold;
                font-size: 13px;
                color: #333;
                border: 2px solid #e0e0e0;
                border-radius: 6px;
                margin-top: 0px;
                padding-top: 12px;
                background-color: white;
            }
        """)
        grid_layout = QGridLayout(box)
        grid_layout.setHorizontalSpacing(16)  # 水平间距
        grid_layout.setVerticalSpacing(10)  # 垂直间距
        grid_layout.setContentsMargins(12, 16, 12, 12)  # 内边距
        
        # 存储属性输入框的字典
        self._attribute_inputs: dict[str, QLineEdit] = {}
        
        # 初始状态：没有属性时显示提示
        self._attribute_form_layout = grid_layout
        self._update_attribute_form()
        
        # 将组框放入滚动区域
        scroll_area.setWidget(box)
        
        # 将滚动区域添加到容器布局，设置拉伸因子让它可以自适应缩放
        container_layout.addWidget(scroll_area, 1)  # 设置拉伸因子为1，占据剩余空间
        
        return container
    
    def _update_attribute_form(self) -> None:
        """更新属性表单，显示当前分类的属性，每行放置两个属性。"""
        # 清除现有表单内容（保留标题）
        while self._attribute_form_layout.count() > 0:
            item = self._attribute_form_layout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()
        
        self._attribute_inputs.clear()
        
        if not self._current_category_attributes:
            # 没有属性时显示提示
            hint_label = QLabel("请先选择一个分类以查看属性")
            hint_label.setStyleSheet("color: #999; font-style: italic; padding: 20px;")
            hint_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            self._attribute_form_layout.addWidget(hint_label, 0, 0, 1, 4)  # 跨4列（两列属性）
            return
        
        # 显示当前分类的属性，每行放置两个属性
        for index, attr in enumerate(self._current_category_attributes):
            row = index // 2  # 每行两个，计算行号
            col = (index % 2) * 2  # 第0个属性在第0列，第1个属性在第2列
            
            label_text = attr.to_form_label()
            label = QLabel(label_text)
            label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)  # key列左对齐，顶部对齐（支持换行）
            label.setMinimumWidth(100)  # 设置标签最小宽度，保持对齐
            # 启用自动换行，让长key可以完整显示
            label.setWordWrap(True)  # 启用自动换行，支持长文本完整显示
            label.setStyleSheet("color: #333; font-weight: 500;")  # 美化标签样式
            # 添加ToolTip，显示完整的key信息（包括resource_name等详细信息）
            tooltip_text = f"属性名称：{attr.nickname}\n资源名称：{attr.resource_name}"
            if attr.units:
                tooltip_text += f"\n单位：{attr.units}"
            if attr.description:
                tooltip_text += f"\n描述：{attr.description}"
            label.setToolTip(tooltip_text)
            
            input_widget = QLineEdit()
            input_widget.setPlaceholderText(f"{attr.value_type} | {attr.point_type}")
            input_widget.setMinimumHeight(28)
            input_widget.setStyleSheet("""
                QLineEdit {
                    padding: 6px 10px;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    background-color: #fff;
                }
                QLineEdit:focus {
                    border: 1px solid #007bff;
                    outline: none;
                }
            """)
            
            self._attribute_inputs[attr.resource_name] = input_widget
            
            # 添加到网格布局：标签在第col列，输入框在第col+1列
            self._attribute_form_layout.addWidget(label, row, col)
            self._attribute_form_layout.addWidget(input_widget, row, col + 1)
        
        # 设置列宽比例，key列（标签列）和value列（输入框列）的比例为4:1，给key列更多空间以支持长文本换行
        self._attribute_form_layout.setColumnStretch(0, 4)  # 第一组标签列（key列），增加比例以支持长文本
        self._attribute_form_layout.setColumnStretch(1, 1)  # 第一组输入框列（value列）
        self._attribute_form_layout.setColumnStretch(2, 4)  # 第二组标签列（key列），增加比例以支持长文本
        self._attribute_form_layout.setColumnStretch(3, 1)  # 第二组输入框列（value列）
        # 设置value列的最小宽度，减小以给key列更多空间
        self._attribute_form_layout.setColumnMinimumWidth(1, 100)  # 第一组输入框最小宽度
        self._attribute_form_layout.setColumnMinimumWidth(3, 100)  # 第二组输入框最小宽度
        # 设置key列的最小宽度，确保有足够空间显示长文本
        self._attribute_form_layout.setColumnMinimumWidth(0, 150)  # 第一组标签列最小宽度
        self._attribute_form_layout.setColumnMinimumWidth(2, 150)  # 第二组标签列最小宽度
    
    def _build_publish_buttons(self) -> QWidget:
        """构建随机发布和一键发布按钮区域。"""
        container = QWidget()
        layout = QHBoxLayout(container)
        layout.setContentsMargins(0, 6, 0, 0)  # 顶部间距
        layout.setSpacing(8)
        
        # 时间间隔输入框
        interval_label = QLabel("时间间隔（秒）：")
        interval_label.setStyleSheet("color: #555; font-weight: 500;")
        self._interval_input = QLineEdit(str(PUBLISH_DEFAULTS.interval_seconds))
        self._interval_input.setMaximumWidth(70)
        self._interval_input.setMinimumHeight(30)
        self._interval_input.setPlaceholderText("秒")
        self._interval_input.setStyleSheet("""
            QLineEdit {
                padding: 6px 10px;
                border: 1px solid #ddd;
                border-radius: 4px;
                background-color: #fff;
            }
            QLineEdit:focus {
                border: 1px solid #007bff;
                outline: none;
            }
        """)
        
        # 随机生成按钮
        self._random_generate_btn = QPushButton("随机生成")
        self._random_generate_btn.setMinimumHeight(30)
        self._random_generate_btn.setMinimumWidth(90)
        self._random_generate_btn.clicked.connect(self._on_random_generate_clicked)
        self._random_generate_btn.setStyleSheet("""
            QPushButton {
                background-color: #17a2b8;
                color: white;
                border: none;
                border-radius: 4px;
                font-weight: 500;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background-color: #138496;
            }
            QPushButton:pressed {
                background-color: #117a8b;
            }
        """)
        
        # 一键清除按钮
        self._clear_all_btn = QPushButton("一键清除")
        self._clear_all_btn.setMinimumHeight(30)
        self._clear_all_btn.setMinimumWidth(90)
        self._clear_all_btn.clicked.connect(self._on_clear_all_clicked)
        self._clear_all_btn.setStyleSheet("""
            QPushButton {
                background-color: #6c757d;
                color: white;
                border: none;
                border-radius: 4px;
                font-weight: 500;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background-color: #5a6268;
            }
            QPushButton:pressed {
                background-color: #545b62;
            }
        """)
        
        # 随机发布按钮
        self._random_publish_btn = QPushButton("随机发布")
        self._random_publish_btn.setMinimumHeight(30)
        self._random_publish_btn.setMinimumWidth(90)
        self._random_publish_btn.clicked.connect(self._on_random_publish_clicked)
        self._random_publish_btn.setStyleSheet("""
            QPushButton {
                background-color: #007bff;
                color: white;
                border: none;
                border-radius: 4px;
                font-weight: 500;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background-color: #0056b3;
            }
            QPushButton:pressed {
                background-color: #004085;
            }
        """)
        
        # 一键发布按钮
        self._batch_publish_btn = QPushButton("一键发布")
        self._batch_publish_btn.setMinimumHeight(30)
        self._batch_publish_btn.setMinimumWidth(90)
        self._batch_publish_btn.clicked.connect(self._on_batch_publish_clicked)
        self._batch_publish_btn.setStyleSheet("""
            QPushButton {
                background-color: #28a745;
                color: white;
                border: none;
                border-radius: 4px;
                font-weight: 500;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background-color: #218838;
            }
            QPushButton:pressed {
                background-color: #1e7e34;
            }
        """)
        
        layout.addWidget(interval_label)
        layout.addWidget(self._interval_input)
        layout.addStretch()
        layout.addWidget(self._random_generate_btn)
        layout.addWidget(self._clear_all_btn)
        layout.addWidget(self._random_publish_btn)
        layout.addWidget(self._batch_publish_btn)
        
        return container

    # ---------------- 数据加载 ---------------- #

    def _clear_all_ui_content(self) -> None:
        """清空所有UI内容和状态，准备重新加载数据。"""
        logger.info("清空所有UI内容，准备重新加载数据")
        
        # 清空分类列（包括分类树和筛选下拉框）
        self._category_column.clear()
        
        # 清空设备列表
        self._device_table.setRowCount(0)
        self._device_table._device_ids = []
        self._device_table._devices = {}
        self._device_table._checkboxes = {}
        
        # 清空搜索框
        if hasattr(self, '_search_input') and self._search_input:
            self._search_input.clear()
        
        # 清空属性表单
        self._current_category_attributes = []
        self._update_attribute_form()
        
        # 重置当前分类和设备状态
        self._current_category_id = None
        self._current_devices = []
        
        # 清空数据仓库引用
        self._repository = None
        
        # 清空发布历史（因为设备ID可能会变化，旧的发布历史可能无法匹配）
        self._device_publish_history.clear()
        
        # 更新状态栏
        self.statusBar().showMessage("已清空内容，准备重新加载数据...", 2000)
        
        logger.info("UI内容清空完成")

    def _try_auto_load_database(self) -> None:
        """在界面显示后尝试自动加载数据库数据。"""
        if has_db_user_config():
            try:
                db_config = self._current_db_config()
                # 设置数据库状态为"连接中"
                self._set_db_status("连接中...", "connecting")
                self.statusBar().showMessage("正在连接数据库...", 2000)
                self._start_repository_loading()
            except ValueError as exc:
                # 配置无效，显示错误
                self._set_db_status("连接失败", "failed")
                QMessageBox.critical(
                    self,
                    "数据库配置错误",
                    f"数据库配置无效，无法加载数据：\n{exc}\n\n请检查数据库配置。"
                )
                logger.error("数据库配置无效：%s", exc)
                self.statusBar().showMessage("数据库配置无效，请检查配置", 0)
        else:
            # 没有数据库配置，显示提示信息（不弹窗，只在状态栏提示）
            logger.info("未找到数据库配置，等待用户手动配置")
            self.statusBar().showMessage("请配置数据库连接以加载数据", 5000)

    def _start_repository_loading(self) -> None:
        """在后台从数据库加载分类与设备数据，避免阻塞首屏。"""
        logger.info("开始启动数据仓库加载流程...")
        
        # 如果之前的加载线程正在运行，先停止它
        # 注意：需要检查线程对象是否仍然有效（可能已被 deleteLater 删除）
        if self._repository_thread:
            try:
                if self._repository_thread.isRunning():
                    logger.info("检测到之前的加载线程正在运行，正在停止...")
                    self._repository_thread.quit()
                    self._repository_thread.wait(2000)  # 等待最多2秒
                    if self._repository_thread.isRunning():
                        logger.warning("之前的加载线程未能及时停止，强制终止")
                        self._repository_thread.terminate()
                        self._repository_thread.wait(1000)
            except RuntimeError as exc:
                # 线程对象可能已被删除，忽略这个错误
                logger.warning("检查之前的加载线程状态时出错（线程可能已被删除）：%s", exc)
                self._repository_thread = None
        
        # 清空所有UI内容和状态，准备重新加载
        logger.info("清空UI内容...")
        self._clear_all_ui_content()
        
        # 禁用UI组件，防止用户在加载过程中操作
        self._category_column.setDisabled(True)
        self._device_table.setDisabled(True)
        
        try:
            db_config = self._current_db_config()
            logger.info(f"数据库配置获取成功，开始创建加载器...")
            self.statusBar().showMessage("正在从数据库加载分类数据...")
            loader = RepositoryLoader(db_config=db_config)
        except ValueError as exc:
            logger.error("数据库配置无效：%s", exc)
            QMessageBox.critical(self, "配置错误", f"数据库配置无效：\n{exc}")
            self.statusBar().showMessage("数据库配置无效，请检查配置", 0)
            self._category_column.setDisabled(False)
            self._device_table.setDisabled(False)
            return
        
        logger.info("创建加载线程...")
        thread = QThread(self)
        loader.moveToThread(thread)

        thread.started.connect(loader.load)
        # 使用 QueuedConnection 确保信号在主线程中处理，避免UI更新问题
        loader.finished.connect(self._on_repository_loaded, Qt.ConnectionType.QueuedConnection)
        loader.finished.connect(thread.quit, Qt.ConnectionType.QueuedConnection)
        loader.finished.connect(loader.deleteLater, Qt.ConnectionType.QueuedConnection)
        loader.failed.connect(self._on_repository_failed, Qt.ConnectionType.QueuedConnection)
        loader.failed.connect(thread.quit, Qt.ConnectionType.QueuedConnection)
        loader.failed.connect(loader.deleteLater, Qt.ConnectionType.QueuedConnection)
        thread.finished.connect(thread.deleteLater)
        
        # 添加线程完成回调，清理线程引用
        def on_repository_thread_finished():
            logger.info("数据仓库加载线程已完成")
            # 清理线程引用，避免后续访问已删除的对象
            if self._repository_thread == thread:
                self._repository_thread = None
                self._repository_loader = None
        
        thread.finished.connect(on_repository_thread_finished)

        self._repository_loader = loader
        self._repository_thread = thread
        logger.info("启动加载线程...")
        thread.start()
        logger.info("加载线程已启动")

    def _on_repository_loaded(self, repository: DeviceRepository) -> None:
        """数据加载完成回调，在主线程中更新UI。"""
        try:
            self._repository = repository
            root_nodes = repository.get_root_nodes()
            all_nodes = repository.get_all_type_nodes()
            
            logger.info(f"数据加载完成：根节点数量={len(root_nodes)}, 总节点数量={len(all_nodes)}, 设备数量={len(repository._devices)}")
            
            if not root_nodes:
                logger.warning("未找到根节点，可能数据为空或数据关联有问题")
                self.statusBar().showMessage("数据加载完成，但未找到分类数据", 5000)
                # 数据加载失败，但连接可能成功，保持当前状态或设置为失败
                self._set_db_status("连接失败", "failed")
                self._category_column.setDisabled(False)
                self._device_table.setDisabled(False)
                return
            
            # 更新UI
            self._category_column.load_categories(root_nodes, all_nodes)
            self._category_column.setDisabled(False)
            self._device_table.setDisabled(False)
            
            # 更新数据库连接状态为"已连接"（数据已成功加载）
            self._set_db_status("已连接", "connected")
            
            # 更新状态栏消息
            device_count = len(repository._devices)
            self.statusBar().showMessage(f"数据加载成功：{len(root_nodes)} 个分类，{device_count} 个设备", 5000)
            
            logger.info("UI更新完成")
        except Exception as exc:  # noqa: BLE001
            logger.exception("更新UI时发生错误：%s", exc)
            # 发生错误，设置状态为失败
            self._set_db_status("连接失败", "failed")
            QMessageBox.critical(
                self,
                "UI更新失败",
                f"数据加载成功，但更新界面时发生错误：\n{exc}\n\n请检查日志文件获取详细信息。"
            )
            self._category_column.setDisabled(False)
            self._device_table.setDisabled(False)

    def _on_repository_failed(self, message: str) -> None:
        # 数据加载失败，更新数据库连接状态
        self._set_db_status("连接失败", "failed")
        QMessageBox.critical(
            self,
            "数据库加载失败",
            f"从数据库加载分类数据失败：\n{message}\n\n请检查：\n1. 数据库连接配置是否正确\n2. 数据库服务是否正常运行\n3. 网络连接是否正常"
        )
        logger.exception("从数据库加载分类数据失败：%s", message)
        # 记录审计日志
        audit_logger.log_error(
            "repository_load_failed",
            message,
            context={"source": "database"},
        )
        self.statusBar().showMessage("数据库加载失败，请检查数据库连接", 0)
        self._category_column.setDisabled(False)
        self._device_table.setDisabled(False)

    # ---------------- 事件绑定 ---------------- #

    def _bind_events(self) -> None:
        self._category_column.category_selected.connect(self._on_category_selected)
        self._device_table.device_activated.connect(self._show_device_detail_dialog)
        self._device_table.detail_requested.connect(self._show_device_detail_dialog)

    # ---------------- 事件处理 ---------------- #

    def _current_mqtt_config(self) -> MQTTConfig:
        host = self._host_input.text().strip()
        if not host:
            raise ValueError("Host 不能为空")

        port_text = self._port_input.text().strip()
        if not port_text:
            raise ValueError("Port 不能为空")

        try:
            port = int(port_text)
        except ValueError as exc:
            raise ValueError("Port 必须是数字") from exc

        username = self._username_input.text().strip() or None
        password_text = self._password_input.text()
        password = password_text or None

        return MQTTConfig(
            host=host,
            port=port,
            username=username,
            password=password,
        )

    def _configure_service_with_current_inputs(self) -> None:
        try:
            config = self._current_mqtt_config()
        except ValueError as exc:
            logger.warning("当前 MQTT 配置无效：%s", exc)
            return
        self._mqtt_service.configure(config)

    def _try_auto_connect_all(self) -> None:
        """在界面显示后异步尝试连接MQTT服务器和数据库。"""
        # 先尝试连接MQTT服务器
        self._try_auto_connect_mqtt()
        # 然后尝试连接数据库（延迟一点，避免同时连接造成资源竞争）
        QTimer.singleShot(200, self._try_auto_load_database)
    
    def _try_auto_connect_mqtt(self) -> None:
        """在界面显示后异步尝试连接MQTT服务器。"""
        # 检查配置文件是否存在
        if not has_mqtt_user_config():
            logger.debug("未找到MQTT配置文件，跳过自动连接")
            return
        
        # 检查配置是否有效（host不为空）
        try:
            config = self._current_mqtt_config()
        except ValueError as exc:
            logger.warning("MQTT配置无效，跳过自动连接：%s", exc)
            return
        
        # 配置有效，尝试自动连接（异步执行，不阻塞UI）
        logger.info("检测到MQTT配置文件，自动尝试连接：%s:%s", config.host, config.port)
        try:
            self._mqtt_service.connect()
        except Exception as exc:  # noqa: BLE001
            # 自动连接失败不弹窗，只记录日志，避免干扰用户
            logger.warning("自动连接MQTT失败：%s", exc)
            # 记录审计日志
            audit_logger.log_error(
                "mqtt_auto_connect_failed",
                str(exc),
                context={
                    "host": config.host,
                    "port": config.port,
                },
            )

    def _on_connect_clicked(self) -> None:
        try:
            config = self._current_mqtt_config()
        except ValueError as exc:
            QMessageBox.warning(self, "配置错误", str(exc))
            # 记录审计日志
            audit_logger.log_error(
                "mqtt_config_invalid",
                str(exc),
            )
            return

        try:
            self._mqtt_service.configure(config)
            save_mqtt_user_config(
                host=config.host,
                port=config.port,
                username=config.username,
                password=config.password,
            )
            # 记录配置保存
            audit_logger.log_config_change(
                "mqtt_user_config",
                new_value=f"{config.host}:{config.port}",
            )
            self._mqtt_service.connect()
        except Exception as exc:  # noqa: BLE001
            logger.exception("MQTT 连接任务启动失败")
            # 记录审计日志
            audit_logger.log_error(
                "mqtt_connect_start_failed",
                str(exc),
                context={
                    "host": config.host,
                    "port": config.port,
                },
            )
            self._set_status("连接失败", "failed")
            QMessageBox.critical(self, "连接失败", f"无法启动 MQTT 连接任务：\n{exc}")
            self.statusBar().showMessage("连接失败，请检查配置", 5000)
            return

    def _on_category_selected(self, node_id: int) -> None:
        """当用户选择分类时，更新当前分类和设备列表。"""
        if not self._repository:
            return
        # 记录当前选中的物模型类别ID
        self._current_category_id = node_id
        # 获取当前分类下的所有设备
        devices = self._repository.get_devices_by_type(node_id)
        self._current_devices = devices  # 保存原始设备列表
        
        # 获取当前分类的属性（从第一个设备获取，因为同一分类下的设备属性相同）
        if devices:
            self._current_category_attributes = devices[0].attributes
        else:
            self._current_category_attributes = []
        
        # 更新属性表单
        self._update_attribute_form()
        
        # 应用当前搜索条件过滤设备
        filtered_devices = self._filter_devices(devices)
        self._device_table.set_devices(filtered_devices)
        self._update_status_message(len(filtered_devices), len(devices))

    def _on_search_text_changed(self, text: str) -> None:
        """搜索框文本变化时，在当前选中的物模型类别下过滤设备列表。"""
        # 确保已选择分类，否则不进行搜索
        if not self._current_category_id or not self._repository:
            return
        if not self._current_devices:
            return
        
        # 确保搜索只在当前分类的设备中进行
        # 重新从仓库获取当前分类的设备，确保数据一致性
        devices = self._repository.get_devices_by_type(self._current_category_id)
        self._current_devices = devices
        
        # 应用搜索过滤
        filtered_devices = self._filter_devices(devices)
        self._device_table.set_devices(filtered_devices)
        self._update_status_message(len(filtered_devices), len(devices))

    def _filter_devices(self, devices: list[Device]) -> list[Device]:
        """根据搜索关键词过滤设备列表，支持多字段模糊匹配。
        
        注意：此方法只会在当前选中的物模型类别下的设备中进行搜索。
        如果未选择分类，将返回空列表或原始设备列表。
        """
        # 确保已选择分类
        if not self._current_category_id:
            return devices
            
        if not hasattr(self, "_search_input") or not self._search_input:
            return devices
        search_text = self._search_input.text().strip().lower()
        if not search_text:
            return devices

        filtered = []
        for device in devices:
            # 双重检查：确保设备属于当前分类（防御性编程）
            if device.type_node_id != self._current_category_id:
                continue
            # 检查多个字段是否包含搜索关键词（不区分大小写）
            if (
                search_text in device.nickname.lower()
                or search_text in device.type_node_name.lower()
                or search_text in device.product_key.lower()
                or search_text in device.device_sn.lower()
                or search_text in (device.device_name or "").lower()
            ):
                filtered.append(device)
        return filtered

    def _update_status_message(self, filtered_count: int, total_count: int) -> None:
        """更新状态栏消息，显示过滤后的设备数量。"""
        if filtered_count == total_count:
            self.statusBar().showMessage(f"当前分类设备数量：{total_count}")
        else:
            self.statusBar().showMessage(f"当前分类设备数量：{filtered_count} / {total_count}")

    @Slot(object)
    def _show_device_detail_dialog(self, device_id: int | object) -> None:
        """显示设备上一次发布的消息详情。"""
        if not self._repository:
            QMessageBox.warning(self, "提示", "分类数据仍在加载，请稍后重试")
            return
        try:
            resolved_id = int(device_id)
        except (TypeError, ValueError):
            QMessageBox.warning(self, "提示", "设备标识无效")
            return

        device = self._repository.get_device(resolved_id)
        if not device:
            QMessageBox.warning(self, "提示", "未找到设备信息")
            return

        # 记录打开设备详情
        audit_logger.log_device_open(
            device.id,
            device.nickname,
            type_name=device.type_node_name,
        )

        # 获取设备的上一次发布历史
        last_publish = self._device_publish_history.get(resolved_id)
        
        # 创建自定义对话框显示消息详情（带滚动条）
        dialog = QDialog(self)
        dialog.setWindowTitle("上一次发布消息详情")
        dialog.resize(500, 400)
        
        layout = QVBoxLayout(dialog)
        layout.setContentsMargins(12, 12, 12, 12)
        layout.setSpacing(8)
        
        if last_publish:
            # 显示上一次发布的消息详情
            info_text = f"设备：{device.nickname}\n"
            info_text += f"Product Key：{device.product_key}\n"
            info_text += f"设备 SN：{device.device_sn}\n"
            info_text += f"主题：{last_publish.get('topic', '')}\n"
            info_text += f"消息ID：{last_publish.get('payload', {}).get('msgid', '')}\n\n"
            info_text += "消息内容：\n"
            payload_json = json.dumps(last_publish.get('payload', {}), ensure_ascii=False, indent=2)
            info_text += payload_json
            
            # 使用 QTextEdit 显示内容，支持滚动
            text_edit = QTextEdit()
            text_edit.setReadOnly(True)
            text_edit.setPlainText(info_text)
            text_edit.setFontFamily("Consolas, 'Courier New', monospace")  # 使用等宽字体
            text_edit.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)  # 不自动换行，保持JSON格式
            
            layout.addWidget(text_edit)
        else:
            # 没有发布历史
            label = QLabel(f"设备：{device.nickname}\n\n该设备还没有发布过消息。")
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            layout.addWidget(label)
        
        # 添加关闭按钮
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        close_btn = QPushButton("关闭")
        close_btn.setMinimumHeight(30)
        close_btn.clicked.connect(dialog.accept)
        button_layout.addWidget(close_btn)
        layout.addLayout(button_layout)
        
        dialog.exec()
    
    def _on_random_generate_clicked(self) -> None:
        """随机生成按钮点击事件。生成随机数据并填充到内容框。"""
        if not self._current_category_attributes:
            QMessageBox.warning(self, "提示", "当前分类没有属性，无法生成数据")
            return
        
        # 使用第一个设备的属性来生成数据（同一分类下的设备属性相同）
        if not self._current_devices:
            QMessageBox.warning(self, "提示", "当前分类没有设备，无法生成数据")
            return
        
        device = self._current_devices[0]
        # 生成随机数据
        payload_dict = self._generator.generate_payload(device, self._current_category_attributes)
        
        # 将随机数据填充到属性测试表单
        self._fill_attribute_form_with_data(self._current_category_attributes, payload_dict)
        
        self.statusBar().showMessage("已生成随机数据并填充到内容框", 2000)
    
    def _on_clear_all_clicked(self) -> None:
        """一键清除按钮点击事件。清除内容框里所有内容。"""
        # 清空所有输入框
        for input_widget in self._attribute_inputs.values():
            input_widget.clear()
        
        self.statusBar().showMessage("已清除所有内容", 2000)
    
    def _on_random_publish_clicked(self) -> None:
        """随机发布按钮点击事件。点击'正在发布'可暂停。"""
        if self._is_random_publishing:
            # 停止随机发布（点击"正在发布"按钮暂停）
            self._random_publish_timer.stop()
            self._is_random_publishing = False
            self._random_publish_btn.setText("随机发布")
            # 恢复按钮原始样式
            self._random_publish_btn.setStyleSheet("""
                QPushButton {
                    background-color: #007bff;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    font-weight: 500;
                    padding: 8px 16px;
                }
                QPushButton:hover {
                    background-color: #0056b3;
                }
                QPushButton:pressed {
                    background-color: #004085;
                }
            """)
            # 清除永久状态栏消息
            self.statusBar().clearMessage()
            self.statusBar().showMessage("随机发布已暂停", 3000)
        else:
            # 开始随机发布
            selected_ids = self._device_table.get_selected_device_ids()
            if not selected_ids:
                QMessageBox.warning(self, "提示", "请先勾选要发布的设备")
                return
            
            if not self._mqtt_service.is_connected():
                QMessageBox.warning(self, "提示", "MQTT 未连接，请先连接 MQTT")
                return
            
            # 获取时间间隔
            try:
                interval = int(self._interval_input.text().strip())
                if interval <= 0:
                    raise ValueError("时间间隔必须大于0")
            except ValueError as exc:
                QMessageBox.warning(self, "提示", f"时间间隔无效：{exc}")
                return
            
            # 启动定时器
            self._random_publish_timer.start(interval * 1000)  # 转换为毫秒
            self._is_random_publishing = True
            self._random_publish_interval = interval  # 保存间隔值
            self._random_publish_btn.setText("正在发布")  # 发布时显示"正在发布"
            # 更新按钮样式，表示正在发布状态
            self._random_publish_btn.setStyleSheet("""
                QPushButton {
                    background-color: #ffc107;
                    color: #333;
                    border: none;
                    border-radius: 4px;
                    font-weight: 600;
                    padding: 8px 16px;
                }
                QPushButton:hover {
                    background-color: #e0a800;
                }
                QPushButton:pressed {
                    background-color: #d39e00;
                }
            """)
            # 状态栏消息设置为0表示永久显示，直到手动清除
            self.statusBar().showMessage(f"随机发布已开始，间隔 {interval} 秒。再次点击'正在发布'可暂停", 0)
    
    def _on_random_publish_timer(self) -> None:
        """随机发布定时器触发事件。"""
        selected_ids = self._device_table.get_selected_device_ids()
        if not selected_ids:
            # 如果没有选中的设备，停止发布
            self._random_publish_timer.stop()
            self._is_random_publishing = False
            self._random_publish_btn.setText("随机发布")
            # 恢复按钮原始样式
            self._random_publish_btn.setStyleSheet("""
                QPushButton {
                    background-color: #007bff;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    font-weight: 500;
                    padding: 8px 16px;
                }
                QPushButton:hover {
                    background-color: #0056b3;
                }
                QPushButton:pressed {
                    background-color: #004085;
                }
            """)
            # 清除永久状态栏消息
            self.statusBar().clearMessage()
            self.statusBar().showMessage("没有选中的设备，随机发布已暂停", 3000)
            return
        
        # 更新状态栏消息，保持暂停提示一直显示
        if self._is_random_publishing:
            self.statusBar().showMessage(
                f"随机发布已开始，间隔 {self._random_publish_interval} 秒。再次点击'正在发布'可暂停", 
                0
            )
        
        # 对每个选中的设备发布随机数据
        for device_id in selected_ids:
            device = self._repository.get_device(device_id) if self._repository else None
            if not device:
                continue
            
            # 生成随机数据并发布
            self._publish_random_data_for_device(device)
    
    def _on_batch_publish_clicked(self) -> None:
        """一键发布按钮点击事件。直接读取内容框里的数据发布，如果缺少数据则报错。"""
        selected_ids = self._device_table.get_selected_device_ids()
        if not selected_ids:
            QMessageBox.warning(self, "提示", "请先勾选要发布的设备")
            return
        
        if not self._mqtt_service.is_connected():
            QMessageBox.warning(self, "提示", "MQTT 未连接，请先连接 MQTT")
            return
        
        # 检查是否有当前分类的属性
        if not self._current_category_attributes:
            QMessageBox.warning(self, "提示", "当前分类没有属性，无法发布")
            return
        
        # 从内容框读取数据并验证
        try:
            payload_dict = self._build_payload_from_form()
        except ValueError as exc:
            QMessageBox.warning(self, "数据验证失败", str(exc))
            return
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(self, "错误", f"构建发布数据时发生错误：\n{exc}")
            return
        
        # 对每个选中的设备发布数据
        count = 0
        for device_id in selected_ids:
            device = self._repository.get_device(device_id) if self._repository else None
            if device:
                self._publish_data_for_device(device, payload_dict)
                count += 1
        
        self.statusBar().showMessage(f"已对 {count} 个设备发起一键发布", 3000)
    
    def _publish_random_data_for_device(self, device: Device) -> None:
        """为指定设备发布随机数据，并将随机数据填充到属性测试表单。"""
        if not device.attributes:
            return
        
        # 生成随机数据
        payload_dict = self._generator.generate_payload(device, device.attributes)
        
        # 将随机数据填充到属性测试表单
        self._fill_attribute_form_with_data(device.attributes, payload_dict)
        
        # 构建MQTT消息（使用配置的消息模板）
        msgid = str(uuid.uuid4())
        try:
            message_str = MessageTemplateValidator.format(
                self._message_template,
                msgid=msgid,
                params=payload_dict,
            )
            payload = json.loads(message_str)
        except Exception as exc:  # noqa: BLE001
            logger.error("格式化消息模板失败，使用默认格式：%s", exc)
            # 如果模板格式化失败，使用默认格式
            payload = {
                "msgid": msgid,
                "params": payload_dict,
            }
        
        # 异步发布（使用配置的Topic模板）
        topic = device.topic(self._topic_template)
        self._mqtt_service.publish_async(topic, payload)
    
    def _publish_data_for_device(self, device: Device, payload_dict: dict[str, dict[str, object]]) -> None:
        """为指定设备发布指定的数据。"""
        # 构建MQTT消息（使用配置的消息模板）
        msgid = str(uuid.uuid4())
        try:
            message_str = MessageTemplateValidator.format(
                self._message_template,
                msgid=msgid,
                params=payload_dict,
            )
            payload = json.loads(message_str)
        except Exception as exc:  # noqa: BLE001
            logger.error("格式化消息模板失败，使用默认格式：%s", exc)
            # 如果模板格式化失败，使用默认格式
            payload = {
                "msgid": msgid,
                "params": payload_dict,
            }
        
        # 异步发布（使用配置的Topic模板）
        topic = device.topic(self._topic_template)
        self._mqtt_service.publish_async(topic, payload)
    
    def _build_payload_from_form(self) -> dict[str, dict[str, object]]:
        """从属性测试表单中读取数据并构建payload。
        
        Returns:
            构建的payload字典，格式为 {resource_name: {"value": value}}
            
        Raises:
            ValueError: 如果所有属性都为空
        """
        if not self._current_category_attributes:
            raise ValueError("当前分类没有属性")
        
        params: dict[str, dict[str, object]] = {}
        
        for attr in self._current_category_attributes:
            resource_name = attr.resource_name
            input_widget = self._attribute_inputs.get(resource_name)
            if not input_widget:
                continue
            
            value_text = input_widget.text().strip()
            if not value_text:
                continue
            
            # 转换值
            value = self._convert_value_smart(attr, value_text)
            params[resource_name] = {
                "value": value,
            }
        
        if not params:
            raise ValueError("内容框中没有数据，请先填写或生成数据后再发布")
        
        return params
    
    def _convert_value_smart(self, attr, text: str):
        """智能转换文本框的值，尝试转换为合适类型但不强制验证。
        
        如果转换失败，直接返回原始字符串，不抛出异常。
        """
        value_type = (attr.value_type or "").strip().lower()
        text_lower = text.strip().lower()
        
        # 首先检查是否是字符串格式的 true 或 false，自动转换为布尔类型
        if text_lower == "true":
            return True
        if text_lower == "false":
            return False
        
        # 尝试转换为布尔值
        if value_type in {"bool", "boolean"}:
            scheme = detect_bool_scheme(attr.units)
            try:
                return coerce_bool_value(text, scheme)
            except ValueError:
                # 转换失败，返回原始字符串
                return text
        
        # 尝试转换为整数
        if value_type in {"int", "int16", "int32", "int64", "uint16", "uint32", "uint64"}:
            try:
                return int(text)
            except ValueError:
                # 转换失败，返回原始字符串
                return text
        
        # 尝试转换为浮点数
        if value_type in {"float", "double", "float32", "float64"}:
            try:
                return float(text)
            except ValueError:
                # 转换失败，返回原始字符串
                return text
        
        # 其他类型直接返回字符串
        return text
    
    def _fill_attribute_form_with_data(
        self, 
        attributes: list, 
        payload_dict: dict[str, dict[str, object]]
    ) -> None:
        """将生成的随机数据填充到属性测试表单中。"""
        for attr in attributes:
            resource_name = attr.resource_name
            if resource_name in payload_dict and resource_name in self._attribute_inputs:
                value = payload_dict[resource_name]["value"]
                # 格式化值以便显示
                formatted_value = self._format_value_for_display(attr, value)
                self._attribute_inputs[resource_name].setText(formatted_value)
    
    def _format_value_for_display(self, attr, value: object) -> str:
        """格式化属性值以便在输入框中显示。"""
        value_type = (attr.value_type or "").strip().lower()
        if value_type in {"bool", "boolean"}:
            scheme = detect_bool_scheme(attr.units)
            return format_bool_for_display(value, scheme)
        return str(value)
    
    @Slot(str, dict, str)
    def _on_mqtt_publish_succeeded(self, topic: str, payload: dict, message_id: str) -> None:
        """MQTT发布成功回调，保存发布历史。"""
        # 从topic中提取device_sn，然后找到对应的设备ID
        # topic格式：v1/{productKey}/{deviceSN}/sys/property/up
        if not self._repository:
            return
        
        try:
            parts = topic.split("/")
            if len(parts) >= 3:
                product_key = parts[1]
                device_sn = parts[2]
                
                # 从repository的所有设备中查找对应的设备
                # 遍历所有设备ID，查找匹配的设备
                for device_id in self._repository._devices.keys():  # noqa: SLF001
                    device = self._repository.get_device(device_id)
                    if device and device.product_key == product_key and device.device_sn == device_sn:
                        # 保存发布历史
                        self._device_publish_history[device.id] = {
                            "topic": topic,
                            "payload": payload,
                            "message_id": message_id,
                        }
                        break
        except Exception as exc:  # noqa: BLE001
            logger.warning("保存发布历史失败：%s", exc)

    # ---------------- MQTT 状态回调 ---------------- #

    def _on_mqtt_connected(self) -> None:
        # 连接成功，清除待显示的失败信息
        self._connection_failed_timer.stop()
        self._pending_failure_info = None
        self._is_showing_failure_dialog = False
        self._set_status("已连接", "connected")
        self.statusBar().showMessage("MQTT 连接成功", 5000)
        # 强制刷新UI，确保状态指示器更新
        self._status_indicator.update()
        self._status_label.update()

    def _on_mqtt_connecting(self) -> None:
        # 重置连接失败相关状态，开始新的连接尝试
        self._connection_failed_timer.stop()
        self._pending_failure_info = None
        self._is_showing_failure_dialog = False
        self._set_status("连接中...", "connecting")
        self.statusBar().showMessage("正在连接 MQTT...", 2000)
        # 强制刷新UI，确保状态指示器更新
        self._status_indicator.update()
        self._status_label.update()

    def _on_mqtt_disconnected(self, rc: int) -> None:
        if rc == 0:
            logger.info("MQTT 正常断开，UI 状态保持等待")
            return
        self._set_status("连接中断", "interrupted")
        self.statusBar().showMessage(f"MQTT 异常断开（错误码：{rc}）", 5000)

    def _on_mqtt_connection_failed(self, rc: int, message: str) -> None:
        """处理 MQTT 连接失败事件。
        
        使用延迟机制确保弹窗只显示最后一次连接失败，避免多次弹窗。
        每次失败都会更新失败信息并重启定时器，确保只显示最新的失败。
        """
        self._set_status("连接失败", "failed")
        detail = message or "未提供错误信息"
        self.statusBar().showMessage(f"MQTT 连接失败（错误码：{rc}）：{detail}", 8000)
        
        # 保存最新的失败信息
        self._pending_failure_info = (rc, detail)
        
        # 停止现有定时器（如果正在运行），然后重新启动
        # 这样可以确保只显示最后一次失败，即使有多个失败信号连续触发
        self._connection_failed_timer.stop()
        self._connection_failed_timer.start(500)
    
    def _show_connection_failed_dialog(self) -> None:
        """显示连接失败弹窗（延迟调用，确保只显示最后一次失败）。"""
        if self._pending_failure_info is None or self._is_showing_failure_dialog:
            return
        
        # 检查是否已经连接成功（避免在连接成功后还显示失败弹窗）
        if self._mqtt_service.is_connected():
            self._pending_failure_info = None
            return
        
        rc, detail = self._pending_failure_info
        self._is_showing_failure_dialog = True
        
        QMessageBox.critical(
            self,
            "连接失败",
            f"无法连接到 MQTT 服务器：\n错误码：{rc}\n详情：{detail}",
        )
        
        # 重置状态
        self._is_showing_failure_dialog = False
        self._pending_failure_info = None

    # ---------------- 生命周期 ---------------- #

    def _center_window(self) -> None:
        """将窗口居中显示在屏幕上（适配1080p屏幕）。"""
        from PySide6.QtWidgets import QApplication
        screen = QApplication.primaryScreen().geometry()
        window_geometry = self.frameGeometry()
        center_point = screen.center()
        window_geometry.moveCenter(center_point)
        self.move(window_geometry.topLeft())

    def closeEvent(self, event) -> None:  # noqa: D401, N802  # type: ignore[override]
        """确保后台线程在窗口关闭时结束。"""
        # 记录应用关闭
        audit_logger.log("app_close", result="success")
        
        # 停止随机发布定时器
        if self._random_publish_timer.isActive():
            self._random_publish_timer.stop()
        
        if self._repository_thread and self._repository_thread.isRunning():
            self._repository_thread.quit()
            self._repository_thread.wait(2000)
        # 关闭 MQTT 服务的线程池
        self._mqtt_service.shutdown()
        # 关闭数据库连接（如果有）
        if self._db_connector:
            try:
                self._db_connector.close()
            except Exception:  # noqa: BLE001
                pass
        super().closeEvent(event)

    def _toggle_password_visibility(self) -> None:
        """切换密码输入框的显示/隐藏状态。"""
        if self._password_visible:
            self._password_input.setEchoMode(QLineEdit.EchoMode.Password)
            self._password_visibility_btn.setText("👁")
            self._password_visible = False
        else:
            self._password_input.setEchoMode(QLineEdit.EchoMode.Normal)
            self._password_visibility_btn.setText("👁‍🗨")
            self._password_visible = True

    def _toggle_username_visibility(self) -> None:
        """切换账号输入框的显示/隐藏状态。"""
        if self._username_visible:
            # 隐藏账号（设置为密码模式）
            self._username_input.setEchoMode(QLineEdit.EchoMode.Password)
            self._username_visibility_btn.setText("👁")
            self._username_visible = False
        else:
            # 显示账号（设置为正常模式）
            self._username_input.setEchoMode(QLineEdit.EchoMode.Normal)
            self._username_visibility_btn.setText("👁‍🗨")
            self._username_visible = True

    def _set_status(
        self,
        text: str,
        state: Literal["connected", "connecting", "disconnected", "failed", "interrupted"],
    ) -> None:
        """更新状态标签与指示灯颜色。"""
        self._status_label.setText(text)

        color_map = {
            "connected": "#28a745",
            "connecting": "#f0ad4e",
            "disconnected": "#dc3545",
            "failed": "#dc3545",
            "interrupted": "#dc3545",
        }
        color = color_map.get(state, "#6c757d")
        indicator_height = self._status_indicator.height() or self._status_indicator.sizeHint().height()
        radius = int(indicator_height / 2)
        self._status_indicator.setStyleSheet(
            (
                "background-color: {color};"
                "border-radius: {radius}px;"
                "border: 1px solid rgba(0, 0, 0, 0.1);"
            ).format(color=color, radius=radius)
        )
        # 强制刷新UI，确保状态指示器立即更新
        self._status_indicator.repaint()
        self._status_label.repaint()

    # ---------------- 数据库连接相关方法 ---------------- #

    def _current_db_config(self) -> dict[str, str | int]:
        """获取当前数据库配置。
        
        Returns:
            数据库配置字典
            
        Raises:
            ValueError: 如果配置无效
        """
        host = self._db_host_input.text().strip()
        if not host:
            raise ValueError("数据库 Host 不能为空")

        port_text = self._db_port_input.text().strip()
        if not port_text:
            raise ValueError("数据库 Port 不能为空")

        try:
            port = int(port_text)
        except ValueError as exc:
            raise ValueError("数据库 Port 必须是数字") from exc

        database = self._db_database_input.text().strip()
        if not database:
            raise ValueError("数据库名称不能为空")

        user = self._db_user_input.text().strip()
        if not user:
            raise ValueError("数据库用户名不能为空")

        password = self._db_password_input.text().strip()
        if not password:
            raise ValueError("数据库密码不能为空")

        return {
            "host": host,
            "port": port,
            "database": database,
            "user": user,
            "password": password,
        }

    def _on_db_connect_clicked(self) -> None:
        """数据库连接按钮点击事件。"""
        try:
            db_config = self._current_db_config()
        except ValueError as exc:
            QMessageBox.warning(self, "配置错误", str(exc))
            audit_logger.log_error("db_config_invalid", str(exc))
            return

        # 保存配置
        try:
            save_db_user_config(
                host=db_config["host"],
                port=db_config["port"],
                database=db_config["database"],
                user=db_config["user"],
                password=db_config["password"],
            )
            audit_logger.log_config_change(
                "db_user_config",
                new_value=f"{db_config['host']}:{db_config['port']}/{db_config['database']}",
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("保存数据库配置失败：%s", exc)

        # 异步测试连接并加载数据
        self._set_db_status("连接中...", "connecting")
        self.statusBar().showMessage("正在连接数据库...", 2000)
        self._test_db_connection_async(db_config)
    
    def _test_db_connection_async(self, db_config: dict) -> None:
        """在后台线程中异步测试数据库连接并加载数据。"""
        # 如果之前的连接测试线程正在运行，先停止它
        # 注意：需要检查线程对象是否仍然有效（可能已被 deleteLater 删除）
        if self._db_test_thread:
            try:
                if self._db_test_thread.isRunning():
                    logger.info("检测到之前的连接测试线程正在运行，正在停止...")
                    self._db_test_thread.quit()
                    self._db_test_thread.wait(1000)  # 等待最多1秒
                    if self._db_test_thread.isRunning():
                        logger.warning("之前的连接测试线程未能及时停止，强制终止")
                        self._db_test_thread.terminate()
                        self._db_test_thread.wait(500)
            except RuntimeError as exc:
                # 线程对象可能已被删除，忽略这个错误
                logger.warning("检查之前的连接测试线程状态时出错（线程可能已被删除）：%s", exc)
                self._db_test_thread = None
        
        class DBConnectionTester(QObject):
            """在后台线程中测试数据库连接。"""
            finished = Signal(bool, str)  # success, message
            
            def __init__(self, db_config: dict) -> None:
                super().__init__()
                self._db_config = db_config
            
            @Slot()
            def test(self) -> None:
                import logging
                test_logger = logging.getLogger(__name__)
                test_logger.info("=" * 50)
                test_logger.info("test() 方法被调用，开始测试数据库连接...")
                test_logger.info(f"当前线程：{QThread.currentThread()}")
                try:
                    test_logger.info(f"连接参数：host={self._db_config['host']}, port={self._db_config['port']}, database={self._db_config['database']}, user={self._db_config['user']}")
                    test_logger.info("开始创建 MySQLConnector 实例（这可能会阻塞，如果连接超时）...")
                    
                    # MySQLConnector 在 __init__ 中会立即尝试连接，连接超时为10秒
                    connector = MySQLConnector(
                        host=self._db_config["host"],
                        port=self._db_config["port"],
                        database=self._db_config["database"],
                        user=self._db_config["user"],
                        password=self._db_config["password"],
                    )
                    test_logger.info("MySQLConnector 实例创建完成，检查连接状态...")
                    
                    if connector.is_connected():
                        test_logger.info("数据库连接测试成功！")
                        connector.close()
                        test_logger.info("数据库连接已关闭")
                        self.finished.emit(True, "连接成功")
                    else:
                        test_logger.error("数据库连接测试失败：连接未建立")
                        self.finished.emit(False, "数据库连接失败")
                except Exception as exc:  # noqa: BLE001
                    test_logger.exception("数据库连接测试失败（异常）：%s", exc)
                    error_msg = str(exc)
                    # 如果错误信息太长，截取前200个字符
                    if len(error_msg) > 200:
                        error_msg = error_msg[:200] + "..."
                    self.finished.emit(False, error_msg)
                finally:
                    test_logger.info("test() 方法执行完成")
                    test_logger.info("=" * 50)
        
        tester = DBConnectionTester(db_config)
        thread = QThread(self)
        tester.moveToThread(thread)
        
        # 添加线程启动确认回调
        def on_thread_started():
            logger.info("线程 started 信号已触发，准备调用 test() 方法...")
            # 直接调用 test() 方法，确保它被执行
            try:
                logger.info("直接调用 tester.test()...")
                tester.test()
            except Exception as exc:  # noqa: BLE001
                logger.exception("直接调用 tester.test() 时发生错误：%s", exc)
        
        # 确保信号连接正确
        # 注意：thread.started 信号应该使用 DirectConnection，因为这是在同一个线程中执行的
        thread.started.connect(on_thread_started, Qt.ConnectionType.DirectConnection)
        # 使用 QueuedConnection 确保信号在主线程中处理，避免UI更新问题
        tester.finished.connect(self._on_db_connection_test_result, Qt.ConnectionType.QueuedConnection)
        tester.finished.connect(thread.quit, Qt.ConnectionType.QueuedConnection)
        tester.finished.connect(tester.deleteLater, Qt.ConnectionType.QueuedConnection)
        thread.finished.connect(thread.deleteLater)
        
        # 添加线程完成回调
        def on_thread_finished():
            logger.info("数据库连接测试线程已完成")
            # 清理线程引用，避免后续访问已删除的对象
            if self._db_test_thread == thread:
                self._db_test_thread = None
        
        thread.finished.connect(on_thread_finished)
        
        self._db_test_thread = thread
        logger.info("准备启动数据库连接测试线程...")
        logger.info(f"线程对象：{thread}, tester对象：{tester}")
        logger.info(f"线程启动前状态：isRunning={thread.isRunning()}, isFinished={thread.isFinished()}")
        thread.start()
        logger.info(f"thread.start() 调用完成，线程启动后状态：isRunning={thread.isRunning()}, isFinished={thread.isFinished()}")
    
    def _on_db_connection_test_result(self, success: bool, message: str) -> None:
        """数据库连接测试结果回调。"""
        logger.info(f"数据库连接测试结果：success={success}, message={message}")
        if success:
            self._set_db_status("已连接", "connected")
            self.statusBar().showMessage("数据库连接成功，开始加载数据...", 2000)
            logger.info("数据库连接成功，开始加载数据仓库...")
            # 开始加载数据
            try:
                self._start_repository_loading()
            except Exception as exc:  # noqa: BLE001
                logger.exception("启动数据加载时发生错误：%s", exc)
                self._set_db_status("加载失败", "failed")
                QMessageBox.critical(self, "加载失败", f"启动数据加载时发生错误：\n{exc}")
                self.statusBar().showMessage("数据加载启动失败", 5000)
        else:
            logger.error("数据库连接失败：%s", message)
            self._set_db_status("连接失败", "failed")
            QMessageBox.critical(self, "连接失败", f"无法连接到数据库：\n{message}")
            self.statusBar().showMessage("数据库连接失败，请检查配置", 5000)
            audit_logger.log_error(
                "db_connect_failed",
                message,
                context={
                    "host": self._db_host_input.text().strip(),
                    "port": self._db_port_input.text().strip(),
                    "database": self._db_database_input.text().strip(),
                },
            )

    def _toggle_db_password_visibility(self) -> None:
        """切换数据库密码输入框的显示/隐藏状态。"""
        if self._db_password_visible:
            self._db_password_input.setEchoMode(QLineEdit.EchoMode.Password)
            self._db_password_visibility_btn.setText("👁")
            self._db_password_visible = False
        else:
            self._db_password_input.setEchoMode(QLineEdit.EchoMode.Normal)
            self._db_password_visibility_btn.setText("👁‍🗨")
            self._db_password_visible = True

    def _set_db_status(
        self,
        text: str,
        state: Literal["connected", "connecting", "disconnected", "failed"],
    ) -> None:
        """更新数据库连接状态标签与指示灯颜色。"""
        self._db_status_label.setText(text)

        color_map = {
            "connected": "#28a745",
            "connecting": "#f0ad4e",
            "disconnected": "#dc3545",
            "failed": "#dc3545",
        }
        color = color_map.get(state, "#6c757d")
        indicator_height = self._db_status_indicator.height() or self._db_status_indicator.sizeHint().height()
        radius = int(indicator_height / 2)
        self._db_status_indicator.setStyleSheet(
            (
                "background-color: {color};"
                "border-radius: {radius}px;"
                "border: 1px solid rgba(0, 0, 0, 0.1);"
            ).format(color=color, radius=radius)
        )
    
    def _on_edit_topic_template_clicked(self) -> None:
        """编辑Topic模板按钮点击事件。"""
        dialog = TemplateEditDialog(
            parent=self,
            template_type="topic",
            initial_template=self._topic_template,
        )
        
        if dialog.exec() == QDialog.DialogCode.Accepted:
            new_template = dialog.get_template()
            try:
                # 保存配置
                save_topic_template_config(template=new_template)
                # 更新当前使用的模板
                self._topic_template = new_template
                # 记录配置变更
                audit_logger.log_config_change(
                    "topic_template",
                    new_value=new_template,
                )
                self.statusBar().showMessage("Topic模板已更新", 3000)
            except Exception as exc:  # noqa: BLE001
                logger.exception("保存Topic模板配置失败：%s", exc)
                QMessageBox.critical(self, "保存失败", f"保存Topic模板配置时发生错误：\n{exc}")
    
    def _on_edit_message_template_clicked(self) -> None:
        """编辑消息模板按钮点击事件。"""
        dialog = TemplateEditDialog(
            parent=self,
            template_type="message",
            initial_template=self._message_template,
        )
        
        if dialog.exec() == QDialog.DialogCode.Accepted:
            new_template = dialog.get_template()
            try:
                # 保存配置
                save_message_template_config(template=new_template)
                # 更新当前使用的模板
                self._message_template = new_template
                # 记录配置变更
                audit_logger.log_config_change(
                    "message_template",
                    new_value=new_template,
                )
                self.statusBar().showMessage("消息模板已更新", 3000)
            except Exception as exc:  # noqa: BLE001
                logger.exception("保存消息模板配置失败：%s", exc)
                QMessageBox.critical(self, "保存失败", f"保存消息模板配置时发生错误：\n{exc}")


